As a Toolkit Capability
Expose a multi-agent workflow as a tool inside a parent agent using the [MultiAgent] attribute. The parent agent can invoke the whole workflow like any other tool call.
Basic Usage
public class ResearchToolkit
{
[MultiAgent("Run a full research and writing pipeline")]
public async Task<AgentWorkflowInstance> ResearchPipeline()
{
return await AgentWorkflow.Create()
.AddAgent("researcher", new AgentConfig
{
SystemInstructions = "Research the topic thoroughly."
})
.AddAgent("writer", new AgentConfig
{
SystemInstructions = "Write a clear answer based on the research."
})
.From("researcher").To("writer")
.BuildAsync();
}
}
var agent = await new AgentBuilder()
.WithToolkit<ResearchToolkit>()
.BuildAsync();The parent agent sees ResearchPipeline as a callable tool. When it invokes it, the full workflow runs and all its events — workflow lifecycle events and each node's agent events — bubble up into the parent's RunAsync stream. There is no separate stream to consume.
Provider Inheritance
Agents inside the workflow that don't have their own Provider configured automatically inherit the parent agent's chat client at execution time — no extra configuration needed.
// These agents will use whatever model the parent agent uses
.AddAgent("researcher", new AgentConfig
{
SystemInstructions = "..."
// No Provider set — inherits from parent
})[MultiAgent] Attribute
| Property | Type | Default | Description |
|---|---|---|---|
Description | string? | Method name | Description shown to the LLM. |
Name | string? | Method name | Custom tool name. |
StreamEvents | bool | true | Stream inner workflow events to the parent's event stream. |
TimeoutSeconds | int | 300 | Workflow execution timeout in seconds. |
[MultiAgent(
"Analyze documents and produce a structured report",
Name = "DocumentAnalysis",
TimeoutSeconds = 600
)]
public async Task<AgentWorkflowInstance> AnalysisPipeline() { ... }Typed metadata variant
For compile-time validation with conditional registration:
public class WorkflowMetadata : IToolMetadata
{
public bool HasResearchCapability { get; set; }
}
public class PipelineToolkit
{
[MultiAgent<WorkflowMetadata>("Full research pipeline")]
[ConditionalFunction("HasResearchCapability")]
public async Task<AgentWorkflowInstance> ResearchPipeline() { ... }
}
var agent = await new AgentBuilder()
.WithToolkit<PipelineToolkit>(new WorkflowMetadata { HasResearchCapability = true })
.BuildAsync();→ See 02.1.4 Tool Dynamic Metadata.md for conditional registration details.
Events
When the parent agent invokes a [MultiAgent] tool, everything the workflow emits flows into the same RunAsync loop you're already consuming — no second stream, no separate subscription.
This means a single await foreach handles the parent agent's own events and the workflow's events interleaved:
await foreach (var evt in agent.RunAsync("Research quantum computing", sessionId: sessionId))
{
switch (evt)
{
// Parent agent streaming its own response
case TextDeltaEvent delta when delta.ExecutionContext?.Depth == 0:
Console.Write(delta.Text);
break;
// Workflow lifecycle — emitted while the tool is running
case WorkflowNodeStartedEvent node:
Console.WriteLine($"\n[{node.NodeId} started]");
break;
case WorkflowNodeCompletedEvent node:
Console.WriteLine($"[{node.NodeId} done — {node.Duration.TotalSeconds:F1}s]");
break;
// Text from a specific workflow node
case TextDeltaEvent delta when delta.ExecutionContext?.AgentName == "writer":
Console.Write(delta.Text);
break;
case MessageTurnFinishedEvent:
Console.WriteLine();
break;
}
}Distinguishing parent vs workflow events
Every AgentEvent carries ExecutionContext with two properties useful for filtering:
AgentName— the name of the specific agent that emitted it ("researcher","writer", etc.)Depth— nesting level.0= the parent agent itself,1= a direct workflow node,2= a SubAgent inside a node, and so onAgentChain— the full name hierarchy from root to the emitting agent, e.g.["ParentAgent", "ResearchPipeline", "writer"]
Workflow lifecycle events (WorkflowStartedEvent, WorkflowNodeStartedEvent, etc.) do not have ExecutionContext — they come from the graph orchestrator, not an agent. Pattern-match on them directly without checking ExecutionContext.
await foreach (var evt in agent.RunAsync(input, sessionId: sessionId))
{
if (evt is AgentEvent agentEvt)
{
var depth = agentEvt.ExecutionContext?.Depth ?? 0;
var name = agentEvt.ExecutionContext?.AgentName ?? "unknown";
// Only show text — indented by nesting level
if (agentEvt is TextDeltaEvent delta)
{
var indent = new string(' ', depth * 2);
Console.Write($"{indent}{delta.Text}");
}
}
// Workflow events have no ExecutionContext — match directly
if (evt is WorkflowCompletedEvent wf)
Console.WriteLine($"\nWorkflow done — {wf.Duration.TotalSeconds:F1}s");
}Disabling workflow event bubbling
Set StreamEvents = false on the attribute to suppress all workflow events from the parent stream — only the final tool result will be visible:
[MultiAgent("Run a full research and writing pipeline", StreamEvents = false)]
public async Task<AgentWorkflowInstance> ResearchPipeline() { ... }→ For the complete workflow event reference, see 06.6 Workflow Events.